﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Caching;

namespace MvcSupplantCaching
{
    /// <summary>
    /// A cache to hold the supplant functions for a given output cache entry.
    /// </summary>
    public class SupplantCache : MemoryCache
    {
        private SupplantCache() : base("supplantCache") { }
        private static readonly SupplantCache SupplantCacheInstance = new SupplantCache();

        /// <summary>
        /// The instance of the SupplantCache.
        /// </summary>
        public static SupplantCache Cache
        {
            get { return SupplantCacheInstance; }
        }

        /// <summary>
        /// Set an item in the SupplantCache.
        /// </summary>
        /// <param name="item">The <see cref="System.Runtime.Caching.CacheItem"/> to set in the cache.</param>
        /// <param name="policy">The cache policy to use for the provided entry.</param>
        public override void Set(System.Runtime.Caching.CacheItem item, CacheItemPolicy policy)
        {
            Set(item.Key, item.Value, policy, item.RegionName);
        }

        /// <summary>
        /// Set an item in the SupplantCache.
        /// </summary>
        /// <param name="key">The cache key for the entry to put into the supplant cache.</param>
        /// <param name="value">The cache item to put into the supplant cache.</param>
        /// <param name="absoluteExpiration">The cache item's absolute expiration date.</param>
        /// <param name="regionName">The cache region to use.</param>
        public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            Set(key, value, new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }, regionName);
        }

        /// <summary>
        /// Set an item in the SupplantCache.
        /// </summary>
        /// <param name="key">The cache key for the entry to put into the supplant cache.</param>
        /// <param name="value">The cache item to put into the supplant cache.</param>
        /// <param name="policy">The cache policy to use for the provided entry.</param>
        /// <param name="regionName">The cache region to use.</param>
        public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            base.Set(CreateKeyWithRegion(key, regionName), value, policy, regionName);
        }

        /// <summary>
        /// Gets an item from the supplant cache.
        /// </summary>
        /// <param name="key">The cache key for the item to retrieve.</param>
        /// <param name="regionName">The cache region the item is in.</param>
        /// <returns>A <see cref="System.Runtime.Caching.CacheItem"/> from the cache for the <paramref name="key"/> provided.</returns>
        public override System.Runtime.Caching.CacheItem GetCacheItem(string key, string regionName = null)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            System.Runtime.Caching.CacheItem temporary = base.GetCacheItem(CreateKeyWithRegion(key, regionName), regionName);
            return temporary == null ? null : new System.Runtime.Caching.CacheItem(key, temporary.Value, regionName);
        }

        /// <summary>
        /// Gets an item from the supplant cache.
        /// </summary>
        /// <param name="key">The cache key for the item to retrieve.</param>
        /// <param name="regionName">The cache region the item is in.</param>
        /// <returns>An <see cref="object"/> representing the cached item.</returns>
        public override object Get(string key, string regionName = null)
        {
            return base.Get(CreateKeyWithRegion(key, regionName), regionName);
        }

        /// <summary>
        /// The default capabilities for this cache.
        /// </summary>
        public override DefaultCacheCapabilities DefaultCacheCapabilities
        {
            get
            {
                return (base.DefaultCacheCapabilities | DefaultCacheCapabilities.CacheRegions);
            }
        }

        /// <summary>
        /// Creates a cache key given an existing <paramref name="key" /> and <paramref name="region" />.
        /// </summary>
        /// <param name="key">The cache key to use.</param>
        /// <param name="region">The region.</param>
        /// <returns></returns>
        public static string CreateKeyWithRegion(string key, string region = null)
        {
            return "region:" + (region ?? "null_region") + ";key=" + key;
        }

        public override object Remove(string key, string regionName = null)
        {
            TraceAddRemove(key, regionName, false);
            return base.Remove(key, regionName);            
        }
       
        public override bool Add(string key, object value, DateTimeOffset absoluteExpiration,
            string regionName = null)
        {
            TraceAddRemove(key, regionName, true);
            return base.Add(key, value, absoluteExpiration, regionName);
        }

        /// <summary>
        /// Aggregates all current items in the cache under a new item with the provided <paramref name="key" />
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="absoluteExpiration">The absolute expiration to use for the created aggregate cache item.</param>
        /// <param name="content">The content the supplant cache is being constructed for.</param>
        /// <returns>The <see cref="MvcSupplantCaching.SupplantCacheItemContainer"/> that the cache was moved to.</returns>
        /// <exception cref="System.ArgumentNullException">key</exception>
        /// <remarks>
        /// Any current <seealso cref="SupplantCacheItemContainer"/>s will be left in the cache, while all other
        /// items will be removed and aggregated into a newly created <seealso cref="SupplantCacheItemContainer"/> with a cache key of <paramref name="key" />.
        /// </remarks>
        public SupplantCacheItemContainer AggregateCache(string key, DateTimeOffset absoluteExpiration, string content)
        {
            if(string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException("key");

            // get all non-container items in the supplant cache
            var cacheItemsToAggregate = this.Where(x => !(x.Value is SupplantCacheItemContainer)).ToList();            

            // Loop over all non-container items in the supplant cache and add them to a list of cache items.
            var aggregateCache = new List<SupplantCacheItem>();
            foreach (var cacheItem in cacheItemsToAggregate)
            {                                
                var item = new SupplantCacheItem
                {
                    Key = cacheItem.Key,
                    Function = cacheItem.Value,
                    StartIndex = content.IndexOf(string.Format(SupplantOutputCacheAttribute.SupplantTokenFormat, cacheItem.Key), StringComparison.InvariantCultureIgnoreCase)
                };

                // add the new cache item to the list
                aggregateCache.Add(item);
                // remove the item from the cache
                Remove(cacheItem.Key);
            }
            aggregateCache = aggregateCache.OrderBy(c => c.StartIndex).ToList();

            // create a container
            var cacheContainer = new SupplantCacheItemContainer
            {
                Key = key,
                CacheItems = aggregateCache
            };

            // Add the function cache list as it's own cached entry with the provided key             
            Add(key, cacheContainer, absoluteExpiration);

            // hand back the supplant cache container
            return cacheContainer;
        }

        private static void TraceAddRemove(string key, string regionName, bool isAdd)
        {
            var methodName = isAdd ? "Add" : "Remove";
            Trace.WriteLine(string.Format("SupplantCache.{2} called for key:{0}, regionName:{1}.", key, regionName,methodName));
            Trace.Flush();
        }

        
    }
}
